#include <stddef.h>
#include <syscall.h>
#include <types.h>
#include <stdio.h>
#include <malloc.h>
#include <ipc.h>
#include <gato/render.h>
#include <list.h>
#include <event.h>
#include <input-event-codes.h>
#include <wm-types.h>
#include "wm-private.h"

struct window_manager_t wm = {
    .cursor = WID_INVALID,
    .update = {0, 0, 0, 0},
};

static void wm_init(surface_t *base)
{
    wm.base[0] = *base;
    wm.bg = RGB(0x8e3a81);
    wm.update = (region_t){0, 0, base->width, base->height};
    wm.event = usys_fifo_create(sizeof(struct wm_render_event_t) * 64);
    init_list_head(&wm.win_list);
}

static void wm_cursor(wid_t id)
{
    wm.cursor = id;
}

wid_t wid_alloc(window_t *w)
{
    wid_t id = WID_INVALID;

    for (wid_t i = 0; i < WID_MAX; i++)
    {
        if (wm.win_ids[i] == NULL)
        {
            id = i;
            w->id = id;
            wm.win_ids[i] = w;
            break;
        }
    }

    return id;
}

void wid_free(wid_t id)
{
    if (id < WID_MAX)
    {
        if (wm.win_ids[id])
        {
            wm.win_ids[id] = NULL;
        }
    }
}

window_t *win_get(wid_t id)
{
    if (id < WID_MAX)
    {
        return wm.win_ids[id];
    }
    return NULL;
}

void wm_raise(wid_t id)
{
    window_t *w = win_get(id);

    if (!w)
        return;

    if (wm.cursor != WID_INVALID)
    {
        window_t *cursor = win_get(wm.cursor);
        list_del_init(&w->node);
        list_add_tail(&w->node, &cursor->node);
    }
    else
        list_add_tail(&w->node, &wm.win_list);
}

static void wm_send_event(wid_t id, wm_event_t e)
{
    window_t *w = win_get(id);

    if (w && w->event > 0)
    {
        usys_fifo_write(w->event, (unsigned char *)&e, sizeof(wm_event_t));
    }
}

void wm_send_render_event(struct wm_render_event_t e)
{
    usys_fifo_write(wm.event, (unsigned char *)&e, sizeof(struct wm_render_event_t));
}

static void wm_flush(void)
{
    window_t *win;

    if (region_isempty(wm.update))
        return;

    surface_clear(wm.base, wm.bg, wm.update.x, wm.update.y, wm.update.w, wm.update.h);

    list_for_each_entry(win, &wm.win_list, node)
    {
        surface_blit_region_xy(wm.base, win->surface, wm.update, win->config.x, win->config.y);
    }

    // list_for_each_entry(win, &wm.win_list, node)
    // {
    //     if (win->config.event_mask & WM_FLUSH_EVENT)
    //     {
    //         wm_send_event(
    //             win->id,
    //             (wm_event_t){
    //                 .type = WM_FLUSH_EVENT,
    //             });
    //     }
    // }

    wm.update = (region_t){0, 0, 0, 0};
}

static void wm_merge_update_region(region_t r)
{
    wm.update = region_union(r, wm.update);
}

static wid_t wm_window_is_hit(int x, int y)
{
    window_t *w = NULL;
    list_for_each_entry_reverse(w, &wm.win_list, node)
    {
        if (region_hit((region_t){w->config.x, w->config.y, w->surface->width, w->surface->height}, x, y))
        {
            if (w->id != wm.cursor)
            {
                return w->id;
            }
        }
    }

    return WID_INVALID;
}

static wid_t wm_window_create(int x, int y, int width, int height, color_t bg)
{
    window_t *w = calloc(1, sizeof(window_t));

    w->config.x = x;
    w->config.y = y;
    w->config.width = width;
    w->config.height = height;
    w->bg = bg;
    w->vmo = usys_vmo_create(width * height * sizeof(color_t), VMO_DATA);
    // w->event = usys_fifo_create(sizeof(struct wm_event_t) * 64);
    init_list_head(&w->node);

    color_t *pixel = (color_t *)usys_vmo_map(w->vmo, NULL, PROT_READ | PROT_WRITE, 0);
    surface_wrap(w->surface, pixel, width, height);
    surface_clear(w->surface, bg, 0, 0, width, height);
    return wid_alloc(w);
}

static void wm_window_raise(wid_t id)
{
    window_t *w = win_get(id);
    window_t *cursor = win_get(wm.cursor);

    list_del_init(&w->node);
    list_add_tail(&w->node, &cursor->node);
    wm_merge_update_region((region_t){w->config.x, w->config.y, w->surface->width, w->surface->height});
}

static wid_t wm_window_click(int x, int y)
{
    wid_t id = wm_window_is_hit(x, y);

    if (id != WID_INVALID)
        wm_window_raise(id);

    return id;
}

static void wm_window_move(wid_t id, int dx, int dy)
{
    window_t *w = win_get(id);
    region_t r1 = {w->config.x, w->config.y, w->surface->width, w->surface->height};
    region_t r2 = {w->config.x + dx, w->config.y + dy, w->surface->width, w->surface->height};
    w->config.x += dx;
    w->config.y += dy;
    wm_merge_update_region(region_union(r1, r2));
}

static bool_t wm_window_show(wid_t id)
{
    window_t *w = win_get(id);

    if (!w)
        return FALSE;

    if (wm.cursor != WID_INVALID)
    {
        window_t *cursor = win_get(wm.cursor);
        list_del_init(&w->node);
        list_add_tail(&w->node, &cursor->node);
    }
    else
    {
        list_del_init(&w->node);
        list_add_tail(&w->node, &wm.win_list);
    }

    wm_merge_update_region((region_t){w->config.x, w->config.y, w->surface->width, w->surface->height});

    return TRUE;
}

static wid_t draw_cursor(int x, int y)
{
    wid_t id = wm_window_create(x, y, 16, 25, ARGB(0));
    window_t *win = win_get(id);
#if QEMU_VIRTIO
    draw_polygon(win->surface, (point_t[]){{0, 0}, {14.2, 14.2}, {8.4, 14.3}, {12.9, 23.1}, {9.6, 25.0}, {4.6, 15.8}, {0, 19.4}}, 7, (style_t){
        fill_color : RGB(0xffffff),
        stroke_color : RGB(0),
        stroke_width : 0.5,
    });
#endif
    wm_window_show(id);
    return id;
}

static void move_cursor(wid_t id, int x, int y)
{
    window_t *w = win_get(id);

    if (x >= 0 && x < wm.base->width && y >= 0 && y < wm.base->height)
    {
        region_t r1 = {w->config.x, w->config.y, w->surface->width, w->surface->height};
        region_t r2 = {x, y, w->surface->width, w->surface->height};

        w->config.x = x;
        w->config.y = y;
        wm_merge_update_region(region_union(r1, r2));
    }
}

void do_wm_event_loop()
{
    u64_t addr = usys_framebuffer_create(0);
    printf("usys_framebuffer_create %p\n", addr);

    uint32_t width, height, stride;
    usys_framebuffer_get_info(&width, &height, &stride);
    printf("[%d %d %d]\n", width, height, stride);
    int prev_mouse_x = width / 2, prev_mouse_y = height / 2;

    surface_t *base = &(surface_t){0};
    surface_wrap(base, (color_t *)addr, width, height);

    wm_init(base);

    wid_t cursor = draw_cursor(prev_mouse_x, prev_mouse_y);
    wm_cursor(cursor);

    wm_flush();
    usys_framebuffer_present();

    int event = usys_event_create();
    int is_press = 0;
    wid_t focused_wid = WID_INVALID;
    wid_t hit_wid = WID_INVALID;

    while (1)
    {
        struct input_event_t e;
        struct kobj_poll_desc_t descs[] = {
            {
                .slot = event,
                .signals = 1,
            },
            {
                .slot = wm.event,
                .signals = 1,
            },
        };

        usys_object_wait_many(descs, 2, NULL);

        int mouse_x = prev_mouse_x;
        int mouse_y = prev_mouse_y;
        int dx = 0, dy = 0;
        while (1)
        {
            int ret = usys_event_read(&e);
            if (ret <= 0)
                break;
            if (e.type == EV_REL)
            {

                if (e.code == REL_X)
                {
                    dx += (int)e.value;
                }
                else if (e.code == REL_Y)
                {
                    dy += (int)e.value;
                }
            }
            else if (e.type == EV_ABS)
            {
                if (e.code == ABS_X)
                {
                    dx = (int)e.value - mouse_x;
                }
                else if (e.code == ABS_Y)
                {
                    dy = (int)e.value - mouse_y;
                }
            }
            else if (e.type == EV_KEY)
            {
                if (e.code == BTN_LEFT || e.code == BTN_RIGHT)
                {
                    if (e.value == 1)
                    {
                        is_press = 1;
                        printf("[down]\n");
                        focused_wid = wm_window_click(mouse_x, mouse_y);
                        if (focused_wid != WID_INVALID)
                        {
                            window_t *win = win_get(focused_wid);
                            wm_send_event(
                                focused_wid,
                                (wm_event_t){
                                    .type = WM_MOUSEBUTTONDOWN_EVENT,
                                    .mouse_buttondown_event = {
                                        .x = mouse_x - win->config.x,
                                        .y = mouse_y - win->config.y,
                                        .code = e.code,
                                    },
                                });
                        }
                    }
                    else
                    {
                        is_press = 0;
                        printf("[up]\n");
                    }
                }
                else
                {
                    if (e.value == 1)
                    {
                        wm_send_event(
                            focused_wid,
                            (wm_event_t){
                                .type = WM_KEYDOWN_EVENT,
                                .keydown_event = {
                                    .code = e.code,
                                },
                            });
                    }
                    else
                    {
                        wm_send_event(
                            focused_wid,
                            (wm_event_t){
                                .type = WM_KEYUP_EVENT,
                                .keyup_event = {
                                    .code = e.code,
                                },
                            });
                    }
                }
            }
        }

        while (1)
        {
            struct wm_render_event_t e;
            int ret = usys_fifo_read(wm.event, &e, sizeof(struct wm_render_event_t));
            if (ret <= 0)
                break;

            if (e.type == WM_UPDATE)
            {
                wm_merge_update_region(e.update_event.region);
            }
        }

        mouse_x = clamp(mouse_x + dx, 1, wm.base->width - 1);
        mouse_y = clamp(mouse_y + dy, 1, wm.base->height - 1);

        int real_dx = mouse_x - prev_mouse_x;
        int real_dy = mouse_y - prev_mouse_y;

        if ((real_dx || real_dy))
        {
            wid_t id = wm_window_is_hit(prev_mouse_x, prev_mouse_y);
            if (id != WID_INVALID)
            {
                if (is_press)
                {
                    wm_window_move(id, real_dx, real_dy);
                }
                window_t *win = win_get(id);

                if (win->config.event_mask & WM_ABS_EVENT)
                {
                    wm_send_event(
                        id,
                        (wm_event_t){
                            .type = WM_ABS_EVENT,
                            .abs_event = {
                                .x = mouse_x - win->config.x,
                                .y = mouse_y - win->config.y,
                                .x_root = mouse_x,
                                .y_root = mouse_y,
                            },
                        });
                }
            }

            {
                window_t *win;
                list_for_each_entry(win, &wm.win_list, node)
                {
                    if (win->config.event_mask & WM_ROOT_ABS_EVENT)
                    {
                        wm_send_event(
                            win->id,
                            (wm_event_t){
                                .type = WM_ROOT_ABS_EVENT,
                                .root_abs_event = {
                                    .x_root = mouse_x,
                                    .y_root = mouse_y,
                                },
                            });
                    }
                }
            }

            if (hit_wid != WID_INVALID && hit_wid != id)
            {
                wm_send_event(hit_wid, (wm_event_t){.type = WM_MOUSE_OUT_EVENT});
            }

            if (id != WID_INVALID && hit_wid != id)
            {
                wm_send_event(id, (wm_event_t){.type = WM_MOUSE_IN_EVENT});
            }

            hit_wid = id;

            move_cursor(cursor, mouse_x, mouse_y);
        }

        prev_mouse_x = mouse_x;
        prev_mouse_y = mouse_y;

        wm_flush();
        usys_framebuffer_present();
    }
}